package com.ywk.base.common.office.excel.exportExcel;


import com.ywk.base.common.office.excel.Anno;
import com.ywk.base.common.office.excel.ExcelField;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.jsoup.helper.StringUtil;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;

/**
 * 导出List数据到excel
 * @author binzec
 */
public class ExportExcel {

	/**
	 * 临时文件存储盘符
	 */
//	private final static String TMP_DICK = Global.getConfig("tmp.path");
	
	/**
	 * 工作薄对象
	 */
	private SXSSFWorkbook wb;
	
	/**
	 * 工作表对象
	 */
	private SXSSFSheet sheet;
	
	/**
	 * 样式列表
	 */
	private Map<String, CellStyle> styles;
	
	/**
	 * 当前行号（从第一行开始 ）
	 */
	private int rownum = 0;
	
	/**
	 * 实体类字段属性集合
	 */
	List<Anno> annotationList = new ArrayList<Anno>();
	
	/**
	 * 1-构造函数1
	 * 2-构造函数2
	 */
	private int exportType;
	
	/**
	 * 自定义excel模板接口
	 */
	public interface InitExcel {
		/**
		 * 初始化excel参数
		 * 返回null就是使用默认
		 */
		public ExcelBean initParam();
		/**
		 * 初始化excel样板
		 */
		public ExcelBean initExcel(ExcelBean excelBean);
	}
	
	/**
	 * 构造函数1（使用［实体类+@ExcelField注解］方式导出）
	 * @param clazz		实体类字节码
	 * @param title		表格大标题（第一行，非必须）
	 * @param notice	说明（第二行，非必须）
	 * @param initExcel	自定义excel样式
	 * @param groups	如果@ExcelField注解写了groups属性，则此处可以按组进行导出（例如某字段定义的组为{1,2}，这里传参groups为{1}，这个字段就可以导出来；如果传参为{3}，则导不出来）（非必须）
	 */
	public ExportExcel(Class<?> clazz, String title, String notice, InitExcel initExcel, String... groups) {
		exportType = 1;
		// 反射暴力取出该实体类的全部字段
		Field[] fs = clazz.getDeclaredFields();
		for (Field f : fs) {
			f.setAccessible(true);
			// 取出每一个字段上面的ExcelField注解
			ExcelField ef = f.getAnnotation(ExcelField.class);
			// 取得出来，且注解的type属性是0或1（0：导出导入；1：仅导出），则通过判断
			if (ef != null && (ef.type() == 0 || ef.type() == 1)) {
				// 如果有规定分组，则把符合分组的字段判断出来
				if (groups != null && groups.length > 0) {
					// 符合分组的，则把该字段加入到集合中（下面两个for循环，原理就是两个数组的比较，只要双方存在相同元素就行）
					boolean inGroup = false;
					for (String g : groups) {
						if (inGroup) {
							break;
						}
						for (String efg : ef.groups()) {
							if (g.equals(efg)) {
								inGroup = true;
								annotationList.add(anno2Bean(ef, f.getName()));
								break;
							}
						}
					}
					// 没有分组规则，则直接把该字段加入到集合中
				} else {
					annotationList.add(anno2Bean(ef, f.getName()));
				}
			}
		}
		// 让字段根据注解中的sort属性从小到大排序下
		Collections.sort(annotationList, new Comparator<Anno>() {
			@Override
			public int compare(Anno o1, Anno o2) {
				return new Integer(o1.getSort()).compareTo(new Integer(o2.getSort()));
			}
		});
		// 初始化excel表格样式，如果有自定义的，就用自定义的；没有就用默认的
		if(initExcel == null) {
			initExcel(title, notice, groups);
		} else {
			ExcelBean eb = initExcel.initParam();
			if (eb == null) {
				// 创建工作簿
				wb = new SXSSFWorkbook(500);
				// 创建表格，取名Export
				sheet = wb.createSheet("Export");
				// 创建表格样式
				styles = createStyles();
				eb = new ExcelBean(wb, sheet, styles, rownum, annotationList);
			}
			eb = initExcel.initExcel(eb);
			this.sheet = (SXSSFSheet) eb.getSheet();
			this.wb = eb.getWb();
			this.rownum = eb.getRownum();
		}
	}
	
	/**
	 * 构造函数2（不使用［实体类+@ExcelField注解］方式导出）
	 * @param annotationList	excel数据规则，从左到右每一个单元格的值对应一个Anno实体类，一行Anno即为annotationList集合（顺序必须与excel一致）
	 * @param title				表格大标题（第一行，非必须）
	 * @param notice			说明（第二行，非必须）
	 * @param initExcel			自定义excel样式
	 * @param groups			如果annotationList集合里的Anno实体类写了groups属性，则此处可以按组进行导出（例如某字段定义的组为{1,2}，这里传参groups为{1}，这个字段就可以导出来；如果传参为{3}，则导不出来）（非必须）
	 */
	public ExportExcel(List<Anno> annotationList, String title, String notice, InitExcel initExcel, String... groups) {
		exportType = 2;
		// 直接把annotationList赋值到成员变量即可
		this.annotationList = annotationList;
		// 初始化excel表格样式，如果有自定义的，就用自定义的；没有就用默认的
		if(initExcel == null) {
			initExcel(title, notice, groups);
		} else {
			ExcelBean eb = initExcel.initParam();
			if (eb == null) {
				// 创建工作簿
				wb = new SXSSFWorkbook(500);
				// 创建表格，取名Export
				sheet = wb.createSheet("Export");
				// 创建表格样式
				styles = createStyles();
				eb = new ExcelBean(wb, sheet, styles, rownum, annotationList);
			}
			eb = initExcel.initExcel(eb);
			this.sheet = (SXSSFSheet) eb.getSheet();
			this.wb = eb.getWb();
			this.rownum = eb.getRownum();
		}
	}
	
	/**
	 * 构造函数1（使用［实体类+@ExcelField注解］方式导出）
	 * @param clazz		实体类字节码
	 * @param title		表格大标题（第一行，非必须）
	 * @param notice	说明（第二行，非必须）
	 * @param groups	如果@ExcelField注解写了groups属性，则此处可以按组进行导出（例如某字段定义的组为{1,2}，这里传参groups为{1}，这个字段就可以导出来；如果传参为{3}，则导不出来）（非必须）
	 */
	public ExportExcel(Class<?> clazz, String title, String notice, String... groups) {
		this(clazz, title, notice, null, groups);
	}
	
	/**
	 * 构造函数2（不使用［实体类+@ExcelField注解］方式导出）
	 * @param annotationList	excel数据规则，从左到右每一个单元格的值对应一个Anno实体类，一行Anno即为annotationList集合（顺序必须与excel一致）
	 * @param title				表格大标题（第一行，非必须）
	 * @param notice			说明（第二行，非必须）
	 * @param groups			如果annotationList集合里的Anno实体类写了groups属性，则此处可以按组进行导出（例如某字段定义的组为{1,2}，这里传参groups为{1}，这个字段就可以导出来；如果传参为{3}，则导不出来）（非必须）
	 */
	public ExportExcel(List<Anno> annotationList, String title, String notice, String... groups) {
		this(annotationList, title, notice, null, groups);
	}
	
	/**
	 * 初始化excel表格样式
	 */
	private void initExcel(String title, String notice, String[] groups) {
		// 存放各列标题集合，从annotationList中取出并循环存入
		List<String> headerList = new ArrayList<String>();
		for (Anno anno : annotationList) {
			String header = anno.getTitle();
			headerList.add(header);
		}
		// 创建工作簿
		wb = new SXSSFWorkbook(500);
		// 创建表格，取名Export
		sheet = wb.createSheet("Export");
		// 创建表格样式
		styles = createStyles();
		// 第一行，添加标题
		if (!StringUtil.isBlank(title)) {
			// 新建第一行
			Row titleRow = sheet.createRow(rownum++);
			// 设置高度为30
			titleRow.setHeightInPoints(30);
			// 新建第一个单元格
			Cell titleCell = titleRow.createCell(0);
			// 设置title行的样式
			titleCell.setCellStyle(styles.get("title"));
			// 赋值内容
			titleCell.setCellValue(title);
			// 看headerList有多少个，就向右合并多少个单元格
			sheet.addMergedRegion(new CellRangeAddress(titleRow.getRowNum(), titleRow.getRowNum(), titleRow.getFirstCellNum(), headerList.size() - 1));
		}
		// 第二行，添加自定义说明文本
		if (!StringUtil.isBlank(notice)) {
			// 新建第二行
			Row noticeRow = sheet.createRow(rownum++);
			// 新建第一个单元格
			Cell noticeCell = noticeRow.createCell(0);
			// 设置title行的样式
			noticeCell.setCellStyle(styles.get("notice"));
			// 赋值内容
			noticeCell.setCellValue(notice);
			// 看headerList有多少个，就向右合并多少个单元格
			sheet.addMergedRegion(new CellRangeAddress(noticeRow.getRowNum(), noticeRow.getRowNum(), noticeRow.getFirstCellNum(), headerList.size() - 1));
		}
		// 第三行，列头行
		// 新建第三行
		Row headerRow = sheet.createRow(rownum++);
		// 设置高度为16
		headerRow.setHeightInPoints(16);
		// 遍历列头集合，循环添加单元格
		for (int i = 0; i < headerList.size(); i++) {
			// 新建单元格
			Cell cell = headerRow.createCell(i);
			// 设置单元格样式
			cell.setCellStyle(styles.get("header"));
			// 把字段上注解的title属性取出来，看是否有**，有则截出来，新建批注
			String[] ss = StringUtils.split(headerList.get(i), "**", 2);
			if (ss.length == 2) {
				cell.setCellValue(ss[0]);
				Comment comment = this.sheet.createDrawingPatriarch().createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 3, 3, (short) 5, 6));
				comment.setString(new XSSFRichTextString(ss[1]));
				cell.setCellComment(comment);
			} else {
				cell.setCellValue(headerList.get(i));
			}
		}
	}

	/**
	 * 往excel里填充数据数组
	 * @param e			一行数据
	 * @param isLast	true-末行；false-非末行
	 */
	@SuppressWarnings("unchecked")
	public <E> ExportExcel setData(E e, boolean isLast) {
		// 列
		int colunm = 0;
		// 取出数据行对象
		Row row = sheet.createRow(rownum++);
		for (Anno anno : annotationList) {
			// 看是哪个构造方法创建了本工具列，采用对应的机制取出列值
			Object val = null;
			try {
				if (exportType == 1) {
					Field f = e.getClass().getDeclaredField(anno.getFieldName());
					f.setAccessible(true);
					val = f.get(e);
				} else if (exportType == 2) {
					Map<String, Object> eMap = (Map<String, Object>) e;
					val = eMap.get(anno.getFieldName());
				}
			} catch (Exception ex) {
				val = "";
			}
			// 当达到最后一行时，通知addCell()方法auto调整宽度
			if(isLast) {
				addCell(row, colunm++, val, anno.getAlign(), anno, true);
			} else {
				addCell(row, colunm++, val, anno.getAlign(), anno, false);
			}
		}
		return this;
	}
	
	/**
	 * 添加一个单元格
	 * @param row			行对象
	 * @param column		第几个单元格（第一格为0）
	 * @param val			单元格赋值
	 * @param align			1：靠左；2：居中；3：靠右
	 * @param anno			字段注解
	 * @param isLast		true-最后一行；false-不是最后一行；最后一行了就开始设置下宽度
	 */
	private void addCell(Row row, int column, Object val, int align, Anno anno, boolean isLast) {
		// 查看是否有格式强制
		int forceClazz = anno.getClazz();
		// 创建一个单元格
		Cell cell = row.createCell(column);
		// 根据align拼接styles的key，得到文本位置对应的样式
		CellStyle style = styles.get("data" + (align >= 1 && align <= 3 ? align : ""));
		// 挨个判断val的类型，做对应的内容赋值处理
		try {
			if (org.springframework.util.StringUtils.isEmpty(val)) {
				cell.setCellValue("");
			} else {
				if (forceClazz == Anno.ANNO_NUMBER) {
					cell.setCellValue(Double.parseDouble(val.toString()));
				} else if (forceClazz == Anno.ANNO_STRING) {
					cell.setCellValue(val.toString());
				} else {
					if (val instanceof String) {
						cell.setCellValue((String) val);
					} else if (val instanceof Integer) {
						cell.setCellValue((Integer) val);
					} else if (val instanceof Long) {
						cell.setCellValue((Long) val);
					} else if (val instanceof Double) {
						cell.setCellValue((Double) val);
					} else if (val instanceof Float) {
						cell.setCellValue((Float) val);
					} else if (val instanceof Date) {
						Date d = (Date) val;
						SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
						String d_ = sdf.format(d);
						if (d_.contains("00:00:00")) {
							d_ = d_.substring(0, 10);
						}
						cell.setCellValue(d_);
					} else if (val instanceof BigDecimal) {
						cell.setCellValue(Double.parseDouble(removeZero(((BigDecimal) val).toString())));
					} else {
						cell.setCellValue(val + "");
					}
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
			cell.setCellValue(val + "");
		}
		cell.setCellStyle(style);
		// 最后一行，先自动调整宽度，因为自动调宽对中文兼容不佳，所以再手动放大2倍
		if (isLast) {
			sheet.trackAllColumnsForAutoSizing();
			sheet.autoSizeColumn(column);
			int colWidth = (int) (sheet.getColumnWidth(column) * 2);
			// 如果太大也不好，顶峰值一万
			if (colWidth > 10000) {
				colWidth = 10000;
			}
			sheet.setColumnWidth(column, colWidth);
		}
	}

	/**
	 * 对小数进行处理，将小数点后没有用的0去掉，例如1.20--》1.2
	 * @param data	数据源
	 * @return		处理后的数据
	 * @author binzec
	 */
	public static String removeZero(String data) {
		if (!StringUtils.isBlank(data)) {
			if (data.indexOf(".") > 0) {
				return data.replaceAll("0+?$", "").replaceAll("[.]$", "");
			} else {
				return data;
			}
		} else {
			return null;
		}
	}
	
	/**
	 * 输出到服务器的临时文件目录里
	 * @param tmpFileName 输出临时文件名
	 */
	/*public ExportExcel write(String tmpFileName) throws Exception {
		File file = new File(TMP_DICK + tmpFileName);
		FileOutputStream fos = new FileOutputStream(file);
		wb.write(fos);
		return this;
	}*/
	
	/**
	 * 清理临时文件
	 */
	public ExportExcel dispose(){
		wb.dispose();
		return this;
	}
	
	/**
	 * 将@ExcelField注解内容封装成实体类返回
	 */
	private Anno anno2Bean(ExcelField ef, String fieldName) {
		return new Anno(fieldName, ef.title(), ef.type(), ef.align(), ef.sort(), ef.groups(), ef.clazz());
	}
	
	/**
	 * 创建表格各个区域的样式
	 * @return 样式列表{"title":"标题行样式","notice":"说明行样式","data":"基本文本样式","header":"列头行样式","data1":"基于data，水平居左","data2":"基于data，水平居中","data3":"基于data，水平居右"}
	 */
	private Map<String, CellStyle> createStyles() {
		// 新建集合，存放各个区域的样式
		Map<String, CellStyle> styles = new HashMap<String, CellStyle>();
		// 创建title行样式
		// ..1.创建单元格样式
		CellStyle style = wb.createCellStyle();
		// ..2.设置水平居中
		style.setAlignment(HorizontalAlignment.CENTER);
		// ..3.设置垂直居中
		style.setVerticalAlignment(VerticalAlignment.CENTER);
		// ..4.创建文字样式
		Font titleFont = wb.createFont();
		// ..5.设置字体为Arial（是一套随同多套微软应用软件所分发的无衬线体TrueType字型）
		titleFont.setFontName("Arial");
		// ..6.设置字体高度为16
		titleFont.setFontHeightInPoints((short) 16);
		// ..7.字体设置为加粗
		titleFont.setBold(true);
		style.setFont(titleFont);
		styles.put("title", style);

		// 创建说明行样式
		style = wb.createCellStyle();
		styles.put("notice", style);
		
		// 创建基本文本样式
		style = wb.createCellStyle();
		// ..1.设置垂直居中
		style.setVerticalAlignment(VerticalAlignment.CENTER);
		// ..2.设置每一个单元格四边框为亮灰色
		style.setBorderRight(BorderStyle.THIN);
		style.setRightBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
		style.setBorderLeft(BorderStyle.THIN);
		style.setLeftBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
		style.setBorderTop(BorderStyle.THIN);
		style.setTopBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
		style.setBorderBottom(BorderStyle.THIN);
		style.setBottomBorderColor(IndexedColors.GREY_50_PERCENT.getIndex());
		// ..3.创建文字样式
		Font dataFont = wb.createFont();
		// ..4.设置字体为Arial（是一套随同多套微软应用软件所分发的无衬线体TrueType字型）
		dataFont.setFontName("Arial");
		// ..5.设置字体高度为10
		dataFont.setFontHeightInPoints((short) 10);
		style.setFont(dataFont);
		// ..6.允许换行
		style.setWrapText(true);
		styles.put("data", style);
		
		// 在基本文本样式上，创建列头行样式
		style = wb.createCellStyle();
		style.cloneStyleFrom(styles.get("data"));
		// ..1.水平居中
		style.setAlignment(HorizontalAlignment.CENTER);
		// ..2.设置前景颜色为亮灰色
		style.setFillForegroundColor(IndexedColors.GREY_50_PERCENT.getIndex());
		// ..3.设置图案样式为全部填充
		style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
		// ..4.设置字体颜色为白色
		Font headerFont = wb.createFont();
		headerFont.setColor(IndexedColors.WHITE.getIndex());
		style.setFont(headerFont);
		styles.put("header", style);
		
		// 在基本文本样式上，创建文本样式1，水平居左
		style = wb.createCellStyle();
		style.cloneStyleFrom(styles.get("data"));
		style.setAlignment(HorizontalAlignment.LEFT);
		styles.put("data1", style);

		// 在基本文本样式上，创建文本样式2，水平居中
		style = wb.createCellStyle();
		style.cloneStyleFrom(styles.get("data"));
		style.setAlignment(HorizontalAlignment.CENTER);
		styles.put("data2", style);

		// 在基本文本样式上，创建文本样式3，水平居右
		style = wb.createCellStyle();
		style.cloneStyleFrom(styles.get("data"));
		style.setAlignment(HorizontalAlignment.RIGHT);
		styles.put("data3", style);
		return styles;
	}
	
}